/**
  * Copyright (c) CSIRO Australia, 2009
  *
  * @author $Author: jia020 $
  * @version $Id: GroupXmlSignatureUtil.java 643 2009-05-20 00:20:55Z jia020 $
  */
package au.csiro.nt.pdsp.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;

import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Supports a group of people to sign and validate a document.
 * All signatures to a document only apply to the content of the document without 
 * including other people's signatures. Similarly, validating a document does not 
 * take other people's signatures into account. 
 * @author cwang
 *
 */
public class GroupXmlSignatureUtil
{
	/**
	 * Sign a document with the given key. If the document contains signatures of other people, 
	 * sign only the part without the existing signatures.
	 * @param theKeyStorePath
	 * @param theKeyStorePasswd
	 * @param theKeyName
	 * @param theDoc
	 * @return
	 */
	public static Document signDoc(String theKeyStorePath, String theKeyStorePasswd, String theKeyName, Document theDoc)
	{
		NodeList nl =
		    theDoc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
		int len = nl.getLength();
		if (len == 0) {
			// sign it directly
			return XmlSignatureUtil.signDoc(theKeyStorePath, theKeyStorePasswd, theKeyName, theDoc);
		}
		
		// remove the existing signatures, sign the document without signatures and add back all the signatures
		LinkedList<Node> temp_nodes = new LinkedList<Node>();
		Node parent_node = null; // the parent node of the signature
		
		for (int i = 0; i < len; i++)
		{
			Node n = nl.item(0);
			parent_node = n.getParentNode();
			Node temp_n = parent_node.removeChild(n);
			temp_nodes.add(temp_n);
		}
		
		Document signed_doc = XmlSignatureUtil.signDoc(theKeyStorePath, theKeyStorePasswd, theKeyName, theDoc);
		
		if (signed_doc == null)
			return null;
		
		NodeList snl = signed_doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
		if (snl == null || snl.getLength() == 0)
			return null;
		parent_node = snl.item(0).getParentNode();
		
		for (Iterator<Node> i = temp_nodes.iterator(); i.hasNext();)
		{
			Node n = i.next();
			parent_node.insertBefore(n, snl.item(0));
		}
		
		return signed_doc;
	}
	
	public static String signDocument(String theKeyStorePath, String theKeyStorePasswd, String theKeyName, Document theDoc)
	{
		Document signed_doc = signDoc(theKeyStorePath, theKeyStorePasswd, theKeyName, theDoc);
		
		if (signed_doc == null)
			return null;
		
		return XmlUtil.documentToString(signed_doc);
	}
	
	/**
	 * Signs an XML document in string type and return a signed XML document in string format.
	 * @param theKeyStorePath
	 * @param theKeyStorePasswd
	 * @param theKeyName
	 * @param theContent
	 * @return
	 */
	public static String signString(String theKeyStorePath, String theKeyStorePasswd, String theKeyName, String theContent)
	{
		try
		{
			Document doc = XmlUtil.stringToDocument(theContent);
			
			return signDocument(theKeyStorePath, theKeyStorePasswd, theKeyName,
					doc);
		} catch (FileNotFoundException e)
		{
			e.printStackTrace();
		} catch (IOException e)
		{
			e.printStackTrace();
		} catch (SAXException e)
		{
			e.printStackTrace();
		} catch (ParserConfigurationException e)
		{
			e.printStackTrace();
		}
		return null;		
	}
	
	/**
	 * Signs an XML document stored in a file with specified name.  
	 * @param theKeyStorePath
	 * @param theKeyStorePasswd
	 * @param theKeyName
	 * @param theInputFileName
	 * @return return a signed XML document in string format
	 */
	public static String signFile(String theKeyStorePath, String theKeyStorePasswd, String theKeyName, String theInputFileName)
	{
		try
		{
			Document doc = XmlUtil.fileToDocument(theInputFileName);
			return signDocument(theKeyStorePath, theKeyStorePasswd, theKeyName,
					doc);

		} catch (FileNotFoundException e)
		{
			e.printStackTrace();
		} catch (IOException e)
		{
			e.printStackTrace();
		} catch (SAXException e)
		{
			e.printStackTrace();
		} catch (ParserConfigurationException e)
		{
			e.printStackTrace();
		}
		return null;
		
	}
	
	/**
	 * Validate a document. If the document contains multiple signatures, validate the document using them 
	 * one by one. The validation does not take other people's signatures into account (signature parts are not
	 * used to calculate the digest of the content).
	 * @param theDoc
	 * @return true if there is no Signature element or all signatures pass validation, false otherwise.
	 */
	public static boolean validateDocument(Document theDoc)
	{
		NodeList nl =
			theDoc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
		
		int len = nl.getLength();
		if (len == 0) 
			return true;
		
		// remove the existing signatures, sign the document without signatures and add back all the signatures
		LinkedList<Node> temp_nodes = new LinkedList<Node>();
		Node parent_node = null; // the parent node of the signature
		
		for (int i = len - 1; i >= 0; i--)
		{
			Node n = nl.item(i);
			parent_node = n.getParentNode();
			Node temp_n = parent_node.removeChild(n);
			temp_nodes.add(temp_n);
		}

		// add signature one by one
		for (Iterator<Node> i = temp_nodes.iterator(); i.hasNext();)
		{
			Node n = i.next();
			parent_node.appendChild(n);

			try
			{
				if (XmlSignatureUtil.validateDocument(theDoc) != 0)
					return false;
			} catch (Exception e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
				return false;
			}
			parent_node.removeChild(n);

		}
		return true;
	}
}
